<?php
namespace zake\foundation\point;

use zake\foundation\able\ProtocolAble;
use zake\foundation\able\reactor\ConnectAble;
use zake\foundation\able\stream\ReadBufferAble;
use zake\foundation\reactor\trigger\connect\WsConnect;
use zake\helper\Console;

/**
 * Class WsProtocol
 * @package zake\server\ws
 * webSocket 服务端协议
 *
 * 二进制：0b
 * 十进制：0d
 * 十六进制：0x
 *
 * \x与0x表示一样的，0x为数字表示，\x为字符表示
 *
 * & 上下都为1时才为1,只要有一个0则是0
 * | 上下有一个为1时即为1，都是0时才为0
 * ^ 上下不同则为1，相同则为0
 * @author liu.bin 2020-04-18 01:33
 */
class WebSocket implements ProtocolAble
{
    /**
     * @var WebSocket | null | ProtocolAble
     */
    private static $singleton = null;

    /**
     * FIN + opCode
     * 文本格式 utf-8
     * @var string
     */
    const BINARY_TYPE_BLOB = "\x81";

    /**
     * FIN + opCode
     * 二进制格式
     * @var string
     */
    const BINARY_TYPE_ARRAY_BUFFER = "\x82";


    /**
     * close flag
     * FIN + opCode
     * @var string
     */
    const BINARY_TYPE_ARRAY_CLOSE = "\x88";


    /**
     * PING flag
     * FIN + opCode
     * 二进制格式
     * @var string
     */
    const BINARY_TYPE_ARRAY_PING = "\x89";


    /**
     * PONG flag
     * FIN + opCode
     * 二进制格式
     * @var string
     */
    const BINARY_TYPE_ARRAY_PONG = "\x8A";




    /**
     * 构造器
     *
     * @author liu.bin 2020-04-03 23:16
     */
    public static function singleton()
    {
        if(is_null(static::$singleton)){
            static::$singleton = new static();
        }
        return static::$singleton;
    }


    /**
     * 计算frameLength
     *
     * @param string $buffer
     * @param ReadBufferAble $connect
     *
     * @return int|false
     *          int - 数据包长度（0-没有获取到长度）
     *          false - 数据包错误，需要关闭当前链接
     * @author liu.bin 2020-05-19 16:29
     */
    public function framePoint($buffer,ReadBufferAble $connect)
    {

        // 校验
        if(empty($buffer)){
            return false;
        }


        // 需要依赖wsConnect
        // 记录当前链接状态：
        //  1-握手状态记录
        //  2-发送握手数据包
        //  3-数据打包方式
        if(!$connect instanceof WsConnect){
            return false;
        }


        // 握手
        if(!$connect->isHandShake()){
            if(false === $this->dealHandShake($buffer,$connect)){
                // 当前握手没处理完，下一个包更不要想了
                return false;
            }
            // 握手操作中，对readBuffer重置，此处需要重新获取
            // 一般情况下，重置后buffer为空[< 6],需要继续读取消息
            $buffer = $connect->getReadBuffer();
        }


        // 消息不够长. 6:最小包的head长度
        $bufferLength = strlen($buffer);
        if($bufferLength < 6){
            return 0;
        }


        /**
         * 此时 buffer已经移除用于握手的head部分
         *
         * FIN  1 bit
         *      用来标记当前数据帧是不是最后一个数据帧，因为一个消息可能会分成多个数据帧来传递，
         *      当然，如果只需要一个数据帧的话，第一个数据帧也就是最后一个。
         *
         * ...  3 bit
         *
         * opCode 4bit
         *      故名思议，操作码，占用4个bit位，也就是一个16进制数，它用来描述要传递的数据是什么或者用来干嘛的，只能为下面这些值：
         *          0x0 标示当前数据帧为分片的数据帧，也就是当一个消息需要分成多个数据帧来传送的时候，需要将opCode设置为0x0。
         *          0x1 标示当前数据帧传递的内容是文本
         *          0x2 标示当前数据帧传递的是二进制内容，不要转换成字符串
         *          0x8 标示请求关闭连接
         *          0x9 标示Ping请求
         *          0xA 标示Pong数据包，当收到Ping请求时自动给回一个Pong
         *      目前协议中就规定了这么多，0x3~0x7以及0xB~0xF都是预留作为其它用途的。
         *
         * MASK 1bit
         *      占用一个bit位，表示数据有没有使用掩码，RFC中有说明，服务端发送给客户端的数据帧不能使用掩码，
         *      客户端发送给服务端的数据帧必须使用掩码。如果一个帧的数据使用了掩码，
         *      那么在Maksing-key部分必须是一个32个bit位的掩码，用来给服务端解码数据。
         *
         * Payload len  7 bit
         *      数据的长度
         *      如果 payload len <= 125，则payload len表示数据包的长度
         *      如果 payload len == 126，则后面两个字节（16bit）表示数据包真实长度
         *      如果 payload len == 127，则后面八个字节（64bit）表示数据包真实长度
         *
         * Masking-key  32 bit
         *      数据掩码，如果MASK（1 bit）设置为0，则该部分可以省略。
         *      如果MASK设置位1，怎Masking-key位一个32位的掩码。
         *
         *
         * 数据包 herder
         *      【payload len <= 125】header = 6
         *      fin(1) + ...(3) + opCode(4) + mask(1) + payload(7) + mask-key(32) = 48 bit = 6 byte
         *
         *      【payload len == 126】header = 8
         *      fin(1) + ...(3) + opCode(4) + mask(1) + payload(7) + data-length(16) + mask-key(32) = 64 bit = 8 byte
         *
         *      【payload len == 126】header = 14
         *      fin(1) + ...(3) + opCode(4) + mask(1) + payload(7) + data-length(64) + mask-key(32) = 112 bit = 14 byte
         */
        // 解析数据(从前两个字节)
        $firstByte      = ord($buffer[0]);
        $secondByte     = ord($buffer[1]);

        $fin            = $firstByte >> 7;      // 1 bit
        $opCode         = $firstByte & 0xf;     // 4 bit (0xf = 0b0000.1111)

        $mask           = $secondByte >> 7;     // 1 bit
        $payloadLen     = $secondByte & 127;    // 7 bit（127 = 0b0111.1111）


        // 客户端需要设置mask标识
        if(empty($mask)){
            return false;
        }


        // 处理opCode 需要依赖wsConnect
        // 1-关闭标识：  false
        // 2-ping包：   发送pong，移除wsHead数据
        // 3-pong包:    移除wsHead数据
        // 4-其它包:     继续处理
        $param                  = [];
        $param['opCode']        = $opCode;
        $param['payLoadLen']    = $payloadLen;
        $result = $this->opCode($param,$connect);
        if(false === $result){
            return false;
        }


        // 根据协议获取 body length
        if($payloadLen <= 125){
            $headLen    = 6;
            $bodyLength = $payloadLen;
        }elseif($payloadLen == 126){
            $headLen = 8;
            if($bufferLength < $headLen){
                return 0;
            }
            $unPack     = unpack('nn/ntotal_len', $buffer);
            $bodyLength = $unPack['total_len'];
        }elseif($payloadLen == 127){
            $headLen = 14;
            if($bufferLength < $headLen){
                return 0;
            }
            $unPack     = unpack('n/N2c', $buffer);
            $bodyLength = $unPack['c1'] * 4294967296 + $unPack['c2'];
        }


        // 当前数据包总长度:frameLength
        $frameLength = $bodyLength + $headLen;
        if( $bufferLength < $frameLength ){
            return 0;
        }


        // fin = 0
        if(empty($fin)){
            $frameBuffer = $connect->shiftReadBuffer($frameLength);
            $connect->pushFrame($frameBuffer);
            return 0;
        }


        // fin = 1,将之前fin=0的数据包重新加载入缓冲区中，并一次性返回
        $allFrameBuffer = "";
        foreach ($connect->frameList() as $frameBuffer){
            $allFrameBuffer .= $frameBuffer;
        }
        $allFrameBuffer .= $buffer;
        $connect->resetReadBuffer($allFrameBuffer);

        // 返回 当前数据报文总长度
        return strlen($allFrameBuffer);
    }


    /**
     * @param array $param
     * @param WsConnect $connect
     *
     * @return bool 反馈粘包信息
     *      true : 继续解析包
     *      false: 已处理opCode，不需要后续解析包
     *
     * @author liu.bin 2020-04-23 15:59
     */
    private function opCode($param,$connect)
    {
        $opCode     = $param['opCode'];
        $payloadLen = $param['payLoadLen'];
        switch ($opCode) {
            case 0x0:
                //
                break;

            case 0x1:
                // Blob package.(文本数据标识)
                break;

            case 0x2:
                // Arraybuffer package.(二进制数据标识)
                break;

            case 0x8:
                // Close package.(关闭标识)
                return false;
            case 0x9:
                // Ping
                if (!$payloadLen) {
                    $headLen = 6;
                    $connect->shiftReadBuffer($headLen);
                    $connect->writeBuffer(pack('H*', '8a00'));
                    return 0;
                }
                break;
            case 0xa:
                // WebSocket Pong
                if (!$payloadLen) {
                    // 清空缓冲区pong，等待下个数据包
                    $headLen = 6;
                    $connect->shiftReadBuffer($headLen);
                    return 0;
                }
                break;
            default :
                // Wrong opCode.
                return false;
        }

        //
        return 0;
    }



    /**
     * webSocket hand shake
     *
     * @param string $buffer
     * @param WsConnect $connect
     * @return bool
     *      true    : 解析完数据包后缓冲区不为空（√）                            |继续解析当前数据包
     *      false   : 数据包不够（×）|| 解析完数据包为空（√）|| 格式错误（×）      |不需要继续当前包的解析
     * @author liu.bin 2020-04-21 10:42
     */
    private function dealHandShake($buffer,WsConnect $connect)
    {
        // not HTTP protocol. 需要关闭当前链接
        if (0 !== strpos($buffer, 'GET')) {
            return false;
        }

        // http head 未结束 \r\n\r\n.
        $headEndPos = strpos($buffer, "\r\n\r\n");
        if (!$headEndPos) {
            return 0;
        }

        // Get Sec-WebSocket-Key.
        if (!preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) {
            // 未匹配到 Sec-WebSocket-Key，发送关闭链接数据包.注：返回false将在readBuffer中关闭
            $connect->writeBuffer($this->closeMessage());
            return false;
        }

        // Send handshake response.发送握手成功的消息
        $connect->writeBuffer($this->shakeMessage($match[1]));

        // Mark handshake complete..
        $connect->handShakeSuccessful();

        // WebSocket data buffer.将head部分去除，只保留消息部分;并写入到sharkFrame中
        $connect->setSharkFrame($connect->shiftReadBuffer($headEndPos + 4));

        //
        if (empty($connect->webSocketType)) {
            $connect->webSocketType = static::BINARY_TYPE_BLOB;
        }


        // 触发握手回调
        if($connect instanceof WsConnect){
            $connect->triggerEvent(WsConnect::EVENT_HAND_SHARK,$connect->getSharkFrame());
        }


        // 握手成功
        return true;
    }



    /**
     * 解包
     * 格式化输出数据包的内容
     *
     * @param string $buffer 完整的包含frame (fin + opCode + mask + payload ...)
     * @return string
     * @author liu.bin 2020-04-28 21:32
     */
    public function decode($buffer,ConnectAble $connect=null)
    {
        if(!($connect instanceof WsConnect)){
            return "";
        }
        $len = ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data  = substr($buffer, 8);
        } elseif($len === 127) {
            $masks = substr($buffer, 10, 4);
            $data  = substr($buffer, 14);
        }else{
            $masks = substr($buffer, 2, 4);
            $data  = substr($buffer, 6);
        }
        $decoded = "";
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }


    /**
     * 打包
     * 可根据connect中的变量，动态设置打包类型
     *
     * @param string $buffer
     * @return string
     */
    public function encode($buffer,ConnectAble $connect=null)
    {
        if (!is_scalar($buffer) || !($connect instanceof WsConnect)) {
            return "";
        }
        // 二进制传输
        $firstByte  = $connect->webSocketType ? $connect->webSocketType : static::BINARY_TYPE_BLOB;
        $len        = strlen($buffer);
        if ($len <= 125) {
            // 1 byte
            $encodeBuffer = $firstByte . chr($len) . $buffer;
        } elseif($len <= 65535) {
            // 2 byte
            $encodeBuffer = $firstByte . chr(126) . pack("n", $len) . $buffer;
        }else{
            // 8 byte
            $encodeBuffer = $firstByte . chr(127) . pack("xxxxN", $len) . $buffer;
        }

        return $encodeBuffer;
    }


    /**
     * 打包ping
     * @return string
     *
     * @author liu.bin 2020-05-11 23:56
     */
    public function encodePing()
    {
        return static::BINARY_TYPE_ARRAY_PING.chr(0);
    }



    /**
     * 打包close
     * @return string
     *
     * @author liu.bin 2020-05-11 23:56
     */
    public function encodeClose()
    {
        return static::BINARY_TYPE_ARRAY_CLOSE.chr(0);
    }



    private function shakeMessage($secWebSocketKey)
    {
        // Calculation websocket key.
        $newKey = base64_encode(sha1($secWebSocketKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
        // Handshake response data.
        $handshake_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $handshake_message .= "Upgrade: websocket\r\n";
        $handshake_message .= "Sec-WebSocket-Version: 13\r\n";
        $handshake_message .= "Connection: Upgrade\r\n";
        $handshake_message .= "Server: zake-13\r\n";
        $handshake_message .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n";
        return $handshake_message;
    }



    private function closeMessage()
    {
        $sendData = "HTTP/1.1 400 Bad Request\r\n\r\n<b>400 Bad Request</b><br>";
        $sendData .= "Sec-WebSocket-Key not found.<br>";
        $sendData .= "This is a WebSocket service and can not be accessed via HTTP.";
        return $sendData;
    }
}
